上一篇文章中,介紹了如何透過一些靜態程式碼分析的工具,搭配品質指標的門檻,來快速找到系統中需要重構的程式。
也稍微的介紹了,重構目標的程式基本功能與樣式。
這一篇文章則要開始進行重構了,保證每一步都相當簡單,大家都可以跟著做到。
上一篇文章:[Day 9]Refactoring legacy code簡介
本系列文章專區
@前言
.aspx的程式碼:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
CodeFile="Product_v0.aspx.cs" Inherits="Product_v0" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="Server">
<div>
<fieldset>
<legend>商品資訊</legend>
<table style="width: 100%;">
<tr>
<td>
商品名稱
</td>
<td>
<asp:TextBox ID="txtProductName" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ErrorMessage="請輸入商品名稱"
ControlToValidate="txtProductName"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
重量
</td>
<td>
<asp:TextBox ID="txtProductWeight" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator3" runat="server" ErrorMessage="請輸入商品重量"
ControlToValidate="txtProductWeight"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
長
</td>
<td>
<asp:TextBox ID="txtProductLength" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator4" runat="server" ErrorMessage="請輸入商品長度"
ControlToValidate="txtProductLength"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
寬
</td>
<td>
<asp:TextBox ID="txtProductWidth" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator5" runat="server" ErrorMessage="請輸入商品寬度"
ControlToValidate="txtProductWidth"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
高
</td>
<td>
<asp:TextBox ID="txtProductHeight" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator6" runat="server" ErrorMessage="請輸入商品高度"
ControlToValidate="txtProductHeight"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
是否需低溫冷藏
</td>
<td>
<asp:RadioButtonList ID="rdoNeedCool" runat="server" RepeatDirection="Horizontal">
<asp:ListItem Value="1">是</asp:ListItem>
<asp:ListItem Value="0">否</asp:ListItem>
</asp:RadioButtonList>
<asp:RequiredFieldValidator ID="RequiredFieldValidator7" runat="server" ErrorMessage="請輸入是否需低溫冷藏" ControlToValidate="rdoNeedCool"></asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>
物流商
</td>
<td>
<asp:DropDownList ID="drpCompany" runat="server">
<asp:ListItem>請選擇</asp:ListItem>
<asp:ListItem Value="1">黑貓</asp:ListItem>
<asp:ListItem Value="2">新竹貨運</asp:ListItem>
<asp:ListItem Value="3">郵局</asp:ListItem>
</asp:DropDownList>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="drpCompany"
InitialValue="請選擇" runat="server" ErrorMessage="請選擇物流商"></asp:RequiredFieldValidator>
</td>
</tr>
</table>
<asp:Button ID="btnCalculate" runat="server" Text="計算運費"
onclick="btnCalculate_Click" />
</fieldset>
</div>
<div>
<fieldset>
<legend>結果</legend>物流商:<asp:Label ID="lblCompany" runat="server"></asp:Label>
<br />
運費:<asp:Label ID="lblCharge" runat="server"></asp:Label>
</fieldset>
</div>
</asp:Content>
網頁畫面如下:
@找到目標後,如何開始重構
重構的循環有幾個階段,分別為綠燈、重構、紅燈、填入。如下圖所示:
當我們想要進行『重構』的動作:
就應該先進行『綠燈』的前置作業:
@重構起手式:口說無憑、錄影存證
要記住,現況的程式碼,雖然彷彿一坨垃圾,但他是可以執行出正確結果的垃圾。寫得再好、再完美的程式,如果無法執行出正確的結果,那也沒啥價值可言。
既然,我們要進行重構,重構的意義就在於:『不改變系統外在行為的條件下,改善系統內部的品質』,改程式很簡單,要確保只影響到我們改的程式,要確保原本的行為沒有改變,這個前提要比改程式重要得多。
所以,這邊透過Selenium IDE,先來幫助我們記錄下來現在可以執行出正確結果的行為。
時間,應該浪費在美好的事物上,而不是每次修改完程式,都還要手動去key in一堆沒意義的資料。用最少的effort,達到自動化的效果。Selenium的使用介紹,請見[Day 8]Integration Testing & Web UI Testing
@步驟
確保現在程式可以執行出正確的結果後,開始錄製腳本:
錄製的腳本如下圖所示:
錄製過程中,請記得要在適當的步驟,加入verify的項目,確保到哪一個步驟時,應該有對應的預期結果。
以這邊的例子來說,就是「當選完物流商,重新點選計算運費時,我們會去驗證物流商的名稱,以及運費的結果,是否符合預期。」
看一下測試腳本,大概就知道測試案例進行了哪些動作,如下圖:
這裡,暫時不需要將腳本轉換成C#的測試程式。因為我們的目的,只是確保重構完成後,原本可以正常執行的程式,仍然可以符合預期般正常執行。
@小結
TDD中循環的三大步驟:紅燈、綠燈、重構。
當切入點為重構時,首先就是要確認系統可以正常運作,接下來建立測試,這個測試建立完成後,應該可以通過測試,也就是第一個綠燈。
即使legacy的程式碼,是屬於物件直接相依,條件判斷邏輯可能也相當複雜或醜陋,沒關係,我們的第一步,就是確保最後的執行結果,仍然符合使用者的需求。
還記得嗎?越抽象、越上層的測試,基本上花的成本越小,但異動的頻率可能也會越高。Selenium在這個例子中,就可以發揮效益比最大的功效。因為我們花的成本,只有再操作一次系統而已。
當建立好了這個可以迅速、可重複、可自動執行的Web UI測試後,就可以當作是進入了TDD循環的綠燈階段。
接下來不管改了什麼程式,動了什麼手腳,即便是傷筋動骨,也可以確保最後產出結果,仍符合使用者預期。
更棒的是,基本上不會發生程式不小心改錯而不知情的情況,如果沒有這重要的起手式,就沒人可以保證修改完的程式是對的。
有了這一層最終的保護,也是我們最終的目的,就比較不會發生程式要上線後才由使用者發現問題的情況。如果,真有這樣的情況發生,代表測試案例不夠周全、完整,要做的應該是增加測試案例,並且設法通過測試案例。
還記得嗎?「程式碼不是寫給developer爽的,程式碼存在的目的,是為了滿足使用者的需求」,而「測試案例,就代表著使用者的需求有沒被涵蓋與驗證完成」
最後,以一句話總結:「重構的第一步,請先建立測試」
@補充
當懂得如何重構,也學會如何作單元測試的朋友,在實務上會碰到的第一個問題,就是一個矛盾的問題。
這不就一環咬著一環嗎?
所以,前面才花了這麼多篇文章來介紹不同層級的測試。
這三步的矛盾點,在於「單元測試」得程式碼具備可測試性,也就是得物件獨立,得物件不直接相依。但沒有測試,又不給重構。
因此,只需要再建立一層更高層級的測試,成本低,穩定性也低(但即使是用過一次即丟,也還是有其價值所在)透過UI的迴歸測試、整合測試,來保護最終結果符合使用者預期,接下來只要小幅度地開始進行重構,直到物件職責分開、相依性分開後,只需要接著建立相關物件的單元測試,那麼整段程式碼的重構循環,也就告一段落了。
如果讀者朋友們,眼前碰到的legacy system refactoring難題就是這個矛盾,just try it! 您也可以很輕鬆地就解決這個矛盾點唷。
hatelove提到:
這不就一環咬著一環嗎?
有的時候真的是這樣!! 一個咬一個! 所以到最後就會變成,能測就測~真的沒辦法~只好先放棄了XD
是啊,這是自己碰到的實務經驗,左手卡右手的情況。
所以希望分享一下這個繞道的簡單解決方式,能突破大家的盲點。